﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Threading;

namespace FourPartHarmony3
{
    /// <summary>
    /// スクロールビューアーの中にキャンバスがあり、キャンバスの一部分が描画される状況で、表示される描画物だけが実体化されるキャンバス。
    /// </summary>
    public class HorizontalVirtualCanvas : VirtualizingPanel, IScrollInfo {
        Canvas mContentCanvas;
        ScrollViewer mScrollViewerAsOwner;
        Rect mVisibleRect = Rect.Empty;
        Size mViewPortSize;
        Size mExtent;
        TranslateTransform mTranslate;
        bool mCanHScroll = false;
        bool mCanVScroll = false;
        HorizontalTree<IVirtualChild> mIndex;
        ObservableCollection<IVirtualChild> mChildren;
        Size mSmallScrollIncrement = new Size(10, 10);
        IList<Rect> mDirtyRegions   = new List<Rect>();
        IList<IVirtualChild> mFocusableObjects = new List<IVirtualChild>();
        private int mFocusedIdx = -1;

        public static DependencyProperty VirtualChildProperty = DependencyProperty.Register("VirtualChild", typeof(IVirtualChild), typeof(HorizontalVirtualCanvas));

        public HorizontalVirtualCanvas() {
            mChildren = new ObservableCollection<IVirtualChild>();
            mChildren.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(OnChildrenCollectionChanged);
            mContentCanvas = new Canvas();

            TransformGroup g = new TransformGroup();
            
            mTranslate = new TranslateTransform();
            g.Children.Add(mTranslate);
            mContentCanvas.RenderTransform = g;

            mTranslate.Changed += new EventHandler(OnTranslateChanged);
            this.Children.Add(mContentCanvas);
        }

        void StartUpdateVisuals() {
            Dispatcher.BeginInvoke((Action)(() => {
                UpdateVisuals();
            }));
        }

        /// <summary>
        /// 画面内の表示物を作成し、画面外の表示物を破棄する。
        /// </summary>
        void UpdateVisuals() {
            if (mIndex == null) {
                this.CalculateExtent();
            }

            int addedNodes = CreateNodes();
            int removedNodes = RemoveNodes();
            removedNodes += GarbageCollectNodes();

            if (addedNodes > 0) {
                InvalidateArrange();
            }
            this.InvalidateVisual();
        }

        /// <summary>
        /// 新たに画面外となった領域に存在する表示物を消す。
        /// </summary>
        private int RemoveNodes() {
            Rect visible = GetVisibleRect();
            int count = 0;

            while (mDirtyRegions.Count > 0) {
                int last = mDirtyRegions.Count - 1;
                Rect dirty = mDirtyRegions[last];
                mDirtyRegions.RemoveAt(last);

                foreach (IVirtualChild n in mIndex.GetNodesInside(dirty)) {
                    UIElement e = n.Visual;
                    if (e != null) {
                        Rect nrect = n.Bounds;
                        if (!nrect.IntersectsWith(visible)) {
                            e.ClearValue(VirtualChildProperty);
                            mContentCanvas.Children.Remove(e);
                            n.DisposeVisual();
                            count++;
                        }
                    }

                }
            }
            return count;
        }

        /// <summary>
        /// キャンバスに残った表示物をすべてスキャンし画面外にいるものを消す。
        /// </summary>
        /// <returns>削除した件数。</returns>
        private int GarbageCollectNodes() {
            int count = 0;
            List<UIElement> removeList = new List<UIElement>();
            foreach (UIElement e in mContentCanvas.Children) {
                var n = e.GetValue(VirtualChildProperty) as IVirtualChild;
                if (n == null) {
                    // IVirtualChildを継承していないUIElement。常に表示する。
                    continue;
                }
                Rect nrect = n.Bounds;
                if (nrect.IntersectsWith(mVisibleRect)) {
                    // 画面内に存在する。
                    continue;
                }
                // 画面内に存在しないのでDisposeする。
                e.ClearValue(VirtualChildProperty);
                removeList.Add(e);
                n.DisposeVisual();
                ++count;
            }
            foreach (var e in removeList) {
                mContentCanvas.Children.Remove(e);
            }
            return count;
        }

        /// <summary>
        /// 画面内に存在する表示物を実体化する。
        /// </summary>
        private int CreateNodes() {
            mVisibleRect = GetVisibleRect();
            int count = 0;
            foreach (IVirtualChild n in mIndex.GetNodesInside(mVisibleRect)) {
                if (n.Visual == null) {
                    UIElement e = n.CreateVisual(this);
                    e.SetValue(VirtualChildProperty, n);
                    Rect bounds = n.Bounds;
                    Canvas.SetLeft(e, bounds.Left);
                    Canvas.SetTop(e, bounds.Top);
                    mContentCanvas.Children.Add(e);
                }

                count++;
            }
            return count;
        }

        Rect GetVisibleRect() {
            // 少し広めの領域を戻す。
            double xstart = (this.HorizontalOffset - mSmallScrollIncrement.Width);
            double ystart = (this.VerticalOffset - mSmallScrollIncrement.Height);
            double xend = (this.HorizontalOffset + (mViewPortSize.Width + (2 * mSmallScrollIncrement.Width)));
            double yend = (this.VerticalOffset + (mViewPortSize.Height + (2 * mSmallScrollIncrement.Height)));
            return new Rect(xstart, ystart, xend - xstart, yend - ystart);
        }

        /// <summary>
        /// mIndexを作り直し、キャンバスの表示物をすべて破棄してから表示物を登録し直す。
        /// </summary>
        private void RebuildVisuals() {
            mIndex = null;
            mVisibleRect = Rect.Empty;
            foreach (UIElement e in mContentCanvas.Children) {
                IVirtualChild n = e.GetValue(VirtualChildProperty) as IVirtualChild;
                if (n != null) {
                    e.ClearValue(VirtualChildProperty);
                    n.DisposeVisual();
                }
            }
            mContentCanvas.Children.Clear();
            InvalidateArrange();
            StartUpdateVisuals();
        }

        public void Clear() {
            mChildren.CollectionChanged -= new System.Collections.Specialized.NotifyCollectionChangedEventHandler(OnChildrenCollectionChanged);

            mChildren.Clear();

            mFocusableObjects.Clear();
            mFocusedIdx = -1;

            mChildren.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(OnChildrenCollectionChanged);
            RebuildVisuals();
        }

        public ObservableCollection<IVirtualChild> VirtualChildren {
            get { return mChildren; }
            set {
                if (value == null) {
                    throw new ArgumentNullException("value");
                }

                mChildren.CollectionChanged -= new System.Collections.Specialized.NotifyCollectionChangedEventHandler(OnChildrenCollectionChanged);

                mChildren = value;

                mFocusableObjects.Clear();
                mFocusedIdx = -1;
                foreach (var c in mChildren) {
                    if (c.Focusable) {
                        mFocusableObjects.Add(c);
                    }
                }
                foreach (var c in mFocusableObjects) {
                    mChildren.Remove(c);
                }

                mChildren.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(OnChildrenCollectionChanged);
                RebuildVisuals();
            }
        }

        public void SetFocusAt(int idx) {
            if (idx < 0 || mFocusableObjects.Count <= idx) {
                return;
            }
            mChildren.CollectionChanged -= new System.Collections.Specialized.NotifyCollectionChangedEventHandler(OnChildrenCollectionChanged);

            if (0 <= mFocusedIdx) {
                mChildren.Remove(mFocusableObjects[mFocusedIdx]);
            }
            mFocusedIdx = idx;
            mChildren.Add(mFocusableObjects[idx]);

            mChildren.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(OnChildrenCollectionChanged);
            RebuildVisuals();
        }

        public int GetFocusedIdx() {
            return mFocusedIdx;
        }

        public void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
            RebuildVisuals();
        }

        public TranslateTransform Translate {
            get { return mTranslate; }
        }

        public Size SmallScrollIncrement {
            get { return mSmallScrollIncrement; }
            set { mSmallScrollIncrement = value; }
        }

        /// <summary>
        /// 表示物を追加する。
        /// </summary>
        public void AddVirtualChild(IVirtualChild c) {
            if (c.Focusable) {
                mFocusableObjects.Add(c);
            } else {
                mChildren.Add(c);
            }
        }

        /// <summary>
        /// タッチイベント。CanvasのMouseUpイベントを渡す
        /// </summary>
        /// <param name="xy"></param>
        /// <returns>タッチされたFocusableオブジェクトのidx。タッチされていない時は-1</returns>
        public int OnTouch(Point xy) {
            xy.Offset(mVisibleRect.Left, mVisibleRect.Top);
            // mFocusableObjectsとxyを当たり判定して当たったChildを表示する
            int idx = 0;
            foreach (var c in mFocusableObjects) {
                if (c.Bounds.Contains(xy)) {
                    // 当たり。
                    SetFocusAt(idx);
                    return idx;
                }
                ++idx;
            }

            return -1;
        }

        /// <summary>
        /// 矩形の中にいる表示物を列挙する。
        /// </summary>
        public IEnumerable<IVirtualChild> GetChildrenIntersecting(Rect bounds) {
            if (mIndex == null) {
                yield break;
            }
            foreach (var node in mIndex.GetNodesInside(bounds)) {
                yield return node;
            }
        }

        public bool HasChildrenIntersecting(Rect bounds) {
            if (mIndex != null) {
                return mIndex.HasNodesInside(bounds);
            }
            return false;
        }

        public int LiveVisualCount {
            get { return mContentCanvas.Children.Count - 1; }
        }
        
        public void OnTranslateChanged(object sender, EventArgs e) {
            OnScrollChanged();
        }

        public void OnScaleChanged(object sender, EventArgs e) {
            OnScrollChanged();
        }

        /// <summary>
        /// 表示物を置くキャンバス。
        /// </summary>
        public Canvas ContentCanvas {
            get { return mContentCanvas; }
        }

        /// <summary>
        /// すべての表示物を収容する面積mExtentを算出する。mExtentが変化した時はmIndexを作りなおす。
        /// </summary>
        public void CalculateExtent() {
            bool rebuild = false;
            if (mIndex == null || mExtent.Width==0 || mExtent.Height == 0 ||
                    double.IsNaN(mExtent.Width) || double.IsNaN(mExtent.Height)) {
                rebuild= true;
                bool first = true;
                Rect extent = new Rect();

                foreach (IVirtualChild c in mChildren) {
                    Rect childBounds = c.Bounds;
                    if (childBounds.Width != 0 && childBounds.Height != 0) {
                        if (double.IsNaN(childBounds.Width) || double.IsNaN(childBounds.Height)) {
                            throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentUICulture,
                                "Child type '{0}' returned NaN bounds", c.GetType().Name));
                        }
                        if (first) {
                            extent = childBounds;
                            first = false;
                        } else {
                            extent = Rect.Union(extent, childBounds);
                        }
                    }
                }
                mExtent = extent.Size;

                // この時点でExtentのサイズが確定するのでmIndexを作りなおす。
                mIndex = new HorizontalTree<IVirtualChild>(new Rect(0, 0, extent.Width, extent.Height));
                foreach (IVirtualChild n in mChildren) {
                    if (n.Bounds.Width > 0 && n.Bounds.Height > 0) {
                        mIndex.Insert(n, n.Bounds);
                    }
                }
            }

            // mContentCanvasの大きさはmExtentの大きさだが、MinHeight、MinWidthよりも小さくはならない。
            double w = Math.Max(mContentCanvas.MinWidth,  mExtent.Width);
            double h = Math.Max(mContentCanvas.MinHeight, mExtent.Height);
            mContentCanvas.Width  = w;
            mContentCanvas.Height = h;

            if (mScrollViewerAsOwner != null) {
                mScrollViewerAsOwner.InvalidateScrollInfo();
            }

            if (rebuild) {
                mVisibleRect = GetVisibleRect();
            }
        }

        protected override Size MeasureOverride(Size availableSize) {
            base.MeasureOverride(availableSize);

            CalculateExtent();

            if (availableSize != mViewPortSize) {
                SetViewportSize(availableSize);
            }

            foreach (UIElement child in this.InternalChildren) {
                var n = child.GetValue(VirtualChildProperty) as IVirtualChild;
                if (n != null) {
                    Rect bounds = n.Bounds;
                    child.Measure(bounds.Size);
                }
            }
            if (double.IsInfinity(availableSize.Width)) {
                return mExtent;
            } else {
                return availableSize;
            }
        }

        protected override Size ArrangeOverride(Size finalSize) {
            base.ArrangeOverride(finalSize);
            
            CalculateExtent();

            if (finalSize != mViewPortSize) {
                SetViewportSize(finalSize);
            }
            
            mContentCanvas.Arrange(new Rect(0, 0, mContentCanvas.Width, mContentCanvas.Height));

            if (mIndex == null) {
                StartUpdateVisuals();
            }
        
            return finalSize;
        }
        
        /// <summary>
        /// このキャンバスの全表示物を収容するサイズ。
        /// </summary>
        public Size Extent {
            get { return mExtent; }
        }

        /// <summary>
        /// ビューポートサイズが変わった。
        /// </summary>
        /// <param name="s">新しいビューポートサイズ。</param>
        void SetViewportSize(Size s) {
            if (s != mViewPortSize) {
                mViewPortSize = s;
                OnScrollChanged();
            }
        }

        #region IScrollInfo Members

        public bool CanHorizontallyScroll {
            get { return mCanHScroll; }
            set { mCanHScroll = value; }
        }

        public bool CanVerticallyScroll {
            get { return mCanVScroll; }
            set { mCanVScroll = value; }
        }

        public double ExtentHeight {
            get { return mExtent.Height; }
        }

        public double ExtentWidth {
            get { return mExtent.Width; }
        }

        public void LineDown() {
            SetVerticalOffset(VerticalOffset + (mSmallScrollIncrement.Height));
        }

        public void LineLeft() {
            SetHorizontalOffset(HorizontalOffset - (mSmallScrollIncrement.Width));
        }

        public void LineRight() {
            SetHorizontalOffset(HorizontalOffset + (mSmallScrollIncrement.Width));
        }

        public void LineUp() {
            SetVerticalOffset(VerticalOffset - (mSmallScrollIncrement.Height));
        }

        public Rect MakeVisible(System.Windows.Media.Visual visual, Rect rectangle) {
            return rectangle;
        }

        public void MouseWheelUp() {
            SetHorizontalOffset(HorizontalOffset - (mSmallScrollIncrement.Width));
        }

        public void MouseWheelDown() {
            SetHorizontalOffset(HorizontalOffset + (mSmallScrollIncrement.Width));
        }

        public void MouseWheelLeft() {
            SetVerticalOffset(VerticalOffset - (mSmallScrollIncrement.Height));
        }

        public void MouseWheelRight() {
            SetVerticalOffset(VerticalOffset + (mSmallScrollIncrement.Height));
        }

        public void PageDown() {
            SetVerticalOffset(VerticalOffset + mViewPortSize.Height);
        }

        public void PageLeft() {
            SetHorizontalOffset(HorizontalOffset - mViewPortSize.Width);
        }

        public void PageRight() {
            SetHorizontalOffset(HorizontalOffset + mViewPortSize.Width);
        }

        public void PageUp() {
            SetVerticalOffset(VerticalOffset - mViewPortSize.Height);
        }

        public ScrollViewer ScrollOwner {
            get { return mScrollViewerAsOwner; }
            set { mScrollViewerAsOwner = value; }
        }

        public void SetHorizontalOffset(double offset) {
            double xoffset = Math.Max(Math.Min(offset, ExtentWidth - ViewportWidth), 0);
            mTranslate.X = -xoffset;
            OnScrollChanged();
        }

        public void SetVerticalOffset(double offset) {
            double yoffset = Math.Max(Math.Min(offset, ExtentHeight - ViewportHeight), 0);
            mTranslate.Y = -yoffset;
            OnScrollChanged();
        }

        public double HorizontalOffset {
            get { return -mTranslate.X; }
        }

        public double VerticalOffset {
            get { return -mTranslate.Y; }
        }

        public double ViewportHeight {
            get { return mViewPortSize.Height; }
        }

        public double ViewportWidth {
            get { return mViewPortSize.Width; }
        }

        #endregion

        /// <summary>
        /// スクロール位置が変更された。描画領域を更新する。
        /// </summary>
        public void OnScrollChanged() {
            Rect dirty = mVisibleRect;
            mVisibleRect = GetVisibleRect();

            Rect intersection = Rect.Intersect(dirty, mVisibleRect);
            if (intersection == Rect.Empty) {
                // 以前表示していた領域と新しく表示する領域が全く重なっていない。
                mDirtyRegions.Add(dirty);
            } else {
                // 以前表示していた領域の左を新しく表示する。
                if (dirty.Left < intersection.Left) {
                    mDirtyRegions.Add(new Rect(dirty.Left, dirty.Top, intersection.Left - dirty.Left, dirty.Height));
                }
                // 以前表示していた領域の右を新しく表示する。
                if (dirty.Right > intersection.Right) {
                    mDirtyRegions.Add(new Rect(intersection.Right, dirty.Top, dirty.Right - intersection.Right, dirty.Height));
                }
                // 以前表示していた領域の上を新しく表示する。
                if (dirty.Top < intersection.Top) {
                    mDirtyRegions.Add(new Rect(dirty.Left, dirty.Top, dirty.Width, intersection.Top - dirty.Top));
                }
                // 以前表示していた領域の下を新しく表示する。
                if (dirty.Bottom > intersection.Bottom) {
                    mDirtyRegions.Add(new Rect(dirty.Left, intersection.Bottom, dirty.Width, dirty.Bottom - intersection.Bottom));
                }
            }

            StartUpdateVisuals();

            if (mScrollViewerAsOwner != null) {
                mScrollViewerAsOwner.InvalidateScrollInfo();
            }
        }
    }
}
